package com.atguigu.exam.service.impl;

import com.atguigu.exam.common.CacheConstants;
import com.atguigu.exam.entity.*;
import com.atguigu.exam.mapper.PaperQuestionMapper;
import com.atguigu.exam.mapper.QuestionAnswerMapper;
import com.atguigu.exam.mapper.QuestionChoiceMapper;
import com.atguigu.exam.mapper.QuestionMapper;
import com.atguigu.exam.service.QuestionService;
import com.atguigu.exam.utils.ExcelUtil;
import com.atguigu.exam.utils.RedisUtils;
import com.atguigu.exam.vo.QuestionImportVo;
import com.atguigu.exam.vo.question.QuestionQueryVo;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 题目Service实现类
 * 实现题目相关的业务逻辑
 */
@Slf4j
@Service
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements QuestionService {
    @Autowired
    private QuestionChoiceMapper questionChoiceMapper;
    @Autowired
    private QuestionAnswerMapper questionAnswerMapper;
    @Autowired
    private PaperQuestionMapper paperQuestionMapper;


    /**
     * 多条件分页题目查询
     * @param questionPage
     * @param questionQueryVo
     */
    @Override
    public void queryPage(Page<Question> questionPage, QuestionQueryVo questionQueryVo) {
//        1.将前端查询参数和查询器绑定，构建动态查询条件
        LambdaQueryWrapper<Question> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(questionQueryVo.getCategoryId() != null,Question::getCategoryId,questionQueryVo.getCategoryId());//如果前端传的题目种类id不为空，在题目表里查询相应题目id
        queryWrapper.eq(!ObjectUtils.isEmpty(questionQueryVo.getDifficulty()),Question::getDifficulty,questionQueryVo.getDifficulty());//如果前端传的难度不为空，在题目表里查询相应难度
        queryWrapper.like(!ObjectUtils.isEmpty(questionQueryVo.getType()),Question::getType,questionQueryVo.getType());//如果前端传的题目类型不为空，在题目表里查询相应题目类型
// like方法用于模糊查询，第二个参数（Question::getTitle）代表要查询的 数据库表字段，第三个参数questionQueryVo.getKeyword()是用户输入的查询关键字（比如用户在搜索框中输入的内容）。MyBatis-Plus 会自动将其拼接为 %关键字% 的形式，如where title like '%Java%'。
        queryWrapper.like(!ObjectUtils.isEmpty(questionQueryVo.getKeyword()),Question::getTitle,questionQueryVo.getKeyword());
        queryWrapper.orderByDesc(Question::getCreateTime);

//        2.执行分页查询
        page(questionPage,queryWrapper);
//        3.关键优化：批量填充选项和答案，避免N+1查询
        fillQuestionsDetailsBatch(questionPage.getRecords());
    }

    @Override
    public Question getQuestionWithDetail(Long id) {
        log.debug("从数据库获取题目详情，题目ID:{}", id);
//        1.查询题目的基本信息
        Question question = this.getById(id);
        if (question == null) return null;
//        2.如果题目存在，且为选择题，查询选项：
        if ("CHOICE".equals(question.getType())) {
            List<QuestionChoice> choices = questionChoiceMapper.selectList(new QueryWrapper<QuestionChoice>().eq("question_id", id));
            question.setChoices(choices);
        }
//        3.根据题目id查询答案
        QuestionAnswer answer = questionAnswerMapper.selectOne(new QueryWrapper<QuestionAnswer>().eq("question_id", id));
//        4.把答案设置进对应题目：
        question.setAnswer(answer);
//        5.缓存记录访问次数（异步处理）
        log.debug("题目详情查询完成，题目ID：{}", id);

        try { //缓存未命中时重试方法
        new Thread(() -> incrementQuestionViewCount(id)).start();
    } catch (Exception e){
            log.warn("增加题目访问计数失败时执行，不影响正常业务，题目ID：{}",id);
        }
        return question;
    }

    /**
     *
     * @param question
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void saveQuestionWithDetails(Question question) {
//        1.保存题目进题目主表
        this.save(question);
        Long questionId = question.getId();
//        2.根据类型处理答案和选项
//          ①如果是选择题
        if ("CHOICE".equals(question.getType())){
//            保存选项
            List<QuestionChoice> choices = question.getChoices();
            if (!CollectionUtils.isEmpty(choices)){
                StringBuilder correctAnswer = new StringBuilder();
                for (int i = 0; i < choices.size(); i++) {
                    QuestionChoice choice = choices.get(i);
                    choice.setQuestionId(questionId);
                    questionChoiceMapper.insert(choice);
//                    如果是正确答案，记录下来：
                    if (choice.getIsCorrect() != null && choice.getIsCorrect()){
                        if (correctAnswer.length() > 0){
                            correctAnswer.append(",");
                        }
//                        选择题第一个答案是A，第二个添加i再转char即可变成B：
                        correctAnswer.append((char) ('A'+i));
                    }
                }
//                保存选择题答案：
                if (correctAnswer.length() > 0){
                    QuestionAnswer answer = new QuestionAnswer();
                    answer.setQuestionId(questionId);
                    answer.setAnswer(correctAnswer.toString());
                    questionAnswerMapper.insert(answer);
                }
            }
        }else { //是判断题和简答题,在答案表里新增答案
            QuestionAnswer answer = question.getAnswer();
            if (answer != null){
                answer.setQuestionId(questionId);
                questionAnswerMapper.insert(answer);
            }
        }
    }

    @Transactional(rollbackFor = Exception.class)
    @Override //思路：更新题目主表信息 -> 删除旧选项，插入新选项 -> 更新或插入答案信息
    public void updateQuestionWithDetais(Question question) {
//        1.更新题目主表
        this.updateById(question);
        Long questionId = question.getId();

//        2.删除旧的答案和选项
        questionAnswerMapper.delete(new QueryWrapper<QuestionAnswer>().eq("question_id",questionId));
        questionChoiceMapper.delete(new QueryWrapper<QuestionChoice>().eq("question_id",questionId));

//        3.插入新的答案和选项
        if ("CHOICE".equals(question.getType())){
//            ①先把前端传的题目选项汇总成list
            List<QuestionChoice> choices = question.getChoices();
//            ②如果前端传来的题目选项不为空
            if (!CollectionUtils.isEmpty(choices)){
//              创建一个字符串构建器，用于拼接正确答案的标识（如 "A,B"）。
                StringBuilder correctAnswer = new StringBuilder();
                for (int i = 0; i < choices.size(); i++) { //循环遍历每一个选项
                    QuestionChoice choice = choices.get(i);
                    choice.setQuestionId(questionId); //为每个选项设置其对应题目id
                    choice.setId(null); /// 清除选项原有的主键 ID。
                    questionChoiceMapper.insert(choice); //插入新的题目选项
                    if (choice.getIsCorrect() != null && choice.getIsCorrect()){
                        if (correctAnswer.length() > 0){ //保存选择题答案
                            correctAnswer.append(",");
                        }
                        correctAnswer.append((char)('A'+i));
                    }
                }
//                保存选择题答案
                if (correctAnswer.length() > 0){
                    QuestionAnswer answer = new QuestionAnswer();
                    answer.setQuestionId(questionId);
                    answer.setAnswer(correctAnswer.toString());
                    answer.setId(null); // 新增：清空答案id，避免重复
                    questionAnswerMapper.insert(answer);
                }
            }
        }else { //是判断题和简答题
            // 判断题和简答题：重点修复这里
            QuestionAnswer answer = question.getAnswer();
            if (answer != null) {
                answer.setQuestionId(questionId);
                answer.setId(null); // 关键修复：强制清空旧id，让框架/数据库生成新id
                questionAnswerMapper.insert(answer);
            }
        }
    }


    @Transactional(rollbackFor = Exception.class)
    @Override //思路：判断试卷有引用题目，删除失败，要先删除子数据（选项和答案）再删除主题目表
    public void removeQuestion(Long id) {
//        1.判断试卷是否有引用题目，有就删除失败！
        LambdaQueryWrapper<PaperQuestion> queryWrapper = new LambdaQueryWrapper<>();
        paperQuestionMapper.delete(queryWrapper.eq(PaperQuestion::getQuestionId, id));
        Long count = paperQuestionMapper.selectCount(queryWrapper);
        if (count > 0){
            throw new RuntimeException("题目有试卷引用，删除失败！");
        }
//        2.先删除该问题id下的子数据：答案和选项
        questionAnswerMapper.delete(new LambdaQueryWrapper<QuestionAnswer>().eq(QuestionAnswer::getQuestionId,id));
        questionChoiceMapper.delete(new LambdaQueryWrapper<QuestionChoice>().eq(QuestionChoice::getQuestionId,id));
//        3.删除主数据
        removeById(id);



    }

    @Override //思路：先查redis缓存，题目数量不完整，使用数据库最新题目进行补充
    public List<Question> getPopularQuestions(Integer size) {
//        1.定义热门题目数据集合
        List<Question> questionList = new ArrayList<>();
//        2.进行redis热门题目id查询（查询热题id集合，id集合不null时查询集合对应题目。根据题目集合长度确定补全题目数，上sql补全题目
//        ①根据积分倒序：截取0 size-1，操作 Sorted Set，从 Redis 中获取 “按访问量倒序排列的热门题目 ID 列表”，
//        zReverseRange 方法的作用是 按分数从高到低（倒序）获取有序集合中的元素
        Set<Object> popularQuestionIds = redisUtils.zReverseRange(CacheConstants.QUESTION_VIEW_COUNT_KEY, 0, size - 1);
//        ②若 popularQuestionIds 不为null，则 safeQuestionIds 就是查询到的集合，若 popularQuestionIds 为null，则 safeQuestionIds 会被赋值为 Collections.emptySet()（一个空的不可变集合）。
        Set<Object> safeQuestionIds = Objects.requireNonNullElse(popularQuestionIds,Collections.emptySet());
        log.info("Redis热题榜查询到的有效id数量：{}",safeQuestionIds.size());
//        3.从数据库查询热门题目（仅当有有效id且size>0时执行
//        将 Redis 中查询到的 “题目 ID” 从原始的Object类型，统一转换为业务中需要的Long类型
        if (size>0 && !safeQuestionIds.isEmpty()){
//            转换id类型：优先强转Long，避免toString性能损耗，过滤无效id
//            从redis里取出来的id可能1是Stirng，可能是long，二者之间不能强转，这里用 Long.valueOf(id.toString())来统一格式成long，并收集为list
            List<Long> questionIds = safeQuestionIds.stream().map(id -> Long.valueOf(id.toString())).collect(Collectors.toList());
//          查询id对应的题目（防护null：若查到null返回空集合，而非空指针异常）
            List<Question> redisQuestionList = Optional.ofNullable(listByIds(questionIds)).orElse(Collections.emptyList());
//            添加到结果集
            questionList.addAll(redisQuestionList);
        }
//        4.补全题目:若热门题目不足，用最新题目补充（确保最终数 <= size）
//        在getPopularQuestion方法中，我们的目标是返回size道热门题目，但redis中热门题目可能不足
//        此时需要从数据库查询最新题目来补充，直到总数达到size
        int needMore = Math.max(0, size - questionList.size());
        if (needMore > 0){
//            收集已查询的题目id
            List<Long> existIds = questionList.stream()
                    .map(q -> (Question) q) // 显式转换为 Question 类型
                    .map(Question::getId)
                    .collect(Collectors.toList());
//            构建查询条件，排除已有题目，按创建时间倒序，限制数量
            LambdaQueryWrapper<Question> queryWrapper = new LambdaQueryWrapper<>();
//            仅当已有id非空时添加排除条件（避免生成无效sql）：
            if (!existIds.isEmpty()){
                queryWrapper.notIn(Question::getId,existIds); //查出非已收集题目id
            }
            queryWrapper.orderByDesc(Question::getCreateTime).last(" LIMIT "+needMore);

//            查询最新题目并补充进结果集：
            List<Question> lastQuestions = this.list(queryWrapper);
            questionList.addAll(lastQuestions);
        }
//        5.批量填充题目详情（如作者、分类等信息）
        fillQuestionsDetailsBatch(questionList);
        return questionList;
    }

    @Override
    public List<QuestionImportVo> previewData(MultipartFile file) throws IOException {
        if (file.isEmpty()){
            throw new RuntimeException("文件不能为空");
        }
        String fileName = file.getOriginalFilename();
        if (fileName == null || (!fileName.endsWith(".xlsx") && !fileName.endsWith(".xls"))){
            throw new RuntimeException("请上传Excel文件（.xlxs或.xls格式）");
        }
//        文件不为空，则解析
        List<QuestionImportVo> questionImportVoList = ExcelUtil.parseExcel(file);
        return questionImportVoList;
    }

    /**
     * 批量题目导入，思路：文件格式校验 -> 文件解析处理 -> 批量数据库添加
     * @param file
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public String batchImportQuestions(MultipartFile file) throws IOException{
//        1.验证文件格式
        if (file.isEmpty()){
            throw new RuntimeException("文件不能为空！");
        }
        String fileName = file.getOriginalFilename();
        if (fileName == null || (!fileName.endsWith(".xlsx") && !fileName.endsWith(".xls"))){
            throw new RuntimeException("请上传Excel文件（.xlsx或.xls格式）");
        }
//        2.解析Excel文件
        List<QuestionImportVo> questionImportVoList = ExcelUtil.parseExcel(file);
        if (questionImportVoList.isEmpty()){
            throw new RuntimeException("Excel文件中没有有效的题目数据！");
        }
//        3.excel有有效题目数据，进行题目批量插入
        int successCount = 0; //定义成功插入的数量
        for (QuestionImportVo importVo : questionImportVoList) {
//            需要利用try避免中断,转换DTO为实体类
            try {
                Question question = convertImportDtoToQuestion(importVo);
//            保存题目及其详细信息：
                saveQuestionWithDetails(question);
                successCount++;

                log.info("成功导入题目:{}",question.getTitle());
            } catch (Exception e) {
                log.error("导入题目失败:{},错误:{}",importVo.getTitle(),e.getMessage());
//                继续处理下一个题目，不中断整个导入过程！
            }
        }
        String message = String.format("Excel导入完成！成功导入 %d / %d 道题目", successCount);

        return message;
    }

    /**
     * 将上传的vo转成quesition进行插入
     * @param importVo
     * @return
     */
    private Question convertImportDtoToQuestion(QuestionImportVo importVo) {
//        该方法负责把从excel解析出来的importVo转成Question实体类：
        Question question = new Question();
//        1.设置基本属性
        question.setTitle(importVo.getTitle());
        question.setType(importVo.getType());
        question.setMulti(importVo.getMulti() != null ? importVo.getMulti() : false);
        question.setCategoryId(importVo.getCategoryId() != null ? importVo.getCategoryId() : 1L);
        question.setDifficulty(importVo.getDifficulty() != null ? importVo.getDifficulty() : "MEDIUM");
        question.setScore(importVo.getScore() != null ? importVo.getScore() : 5);

        question.setAnalysis(importVo.getAnalysis());
        question.setCreateTime(new Date());
        question.setUpdateTime(new Date());

//        处理选择题选项：
        if ("CHOICE".equals(importVo.getType()) && !CollectionUtils.isEmpty(importVo.getChoices())) {
            List<QuestionChoice> choices = new ArrayList<>();
            for (QuestionImportVo.ChoiceImportDto choiceDto : importVo.getChoices()) {
                QuestionChoice choice = new QuestionChoice();
                choice.setContent(choiceDto.getContent());
                choice.setIsCorrect(choiceDto.getIsCorrect() != null ? choiceDto.getIsCorrect() : false);
                choice.setSort(choiceDto.getSort() != null ? choiceDto.getSort() : choices.size() + 1);
                choices.add(choice);
            }
            question.setChoices(choices);//把选择题选项塞进问题里
        }else if (importVo.getAnswer() != null){ // 含有判断题和简答题的答案
            QuestionAnswer answer = new QuestionAnswer();
            answer.setAnswer(importVo.getAnswer());
            answer.setKeywords(importVo.getKeywords());
            question.setAnswer(answer);
        }
        return question;
    }


    @Autowired
    private RedisUtils redisUtils;

    /**
     * 增加题目访问计数
     */
    private void incrementQuestionViewCount(Long questionId) {
        if (!getBaseMapper().exists(new LambdaQueryWrapper<Question>().eq(Question::getId,questionId))){
            log.warn("尝试增加不存在题目的访问计数，题目id:{}",questionId);
            return;
        }
        //增加题目访问计数，如果不存在则初始化为1
//        这里使用redis Sorted Set记录题目访问次数，使用zIncrementScore方法原子性地增加题目访问计数
//        key：访问次数   value：题目id
        Double newScore = redisUtils.zIncrementScore(CacheConstants.QUESTION_VIEW_COUNT_KEY, questionId, 1);
        log.debug("题目访问计数增加，题目id:{},当前计数:{}",questionId,newScore.intValue());
    }

    /**
     * 批量填充题目列表，避免N+1次查询
     */
    private void fillQuestionsDetailsBatch(List<Question>questions){
        if (questions == null || questions.isEmpty()) return;

//        1.如果传来的题目列表不为空，收集所有题目id，形成一个id列表，方便后续集中查询
        List<Long> ids = questions.stream().map(Question::getId).toList();
//        2.批量查询这些题目id下的所有选项
        List<QuestionChoice> allchoices = questionChoiceMapper.selectList(new LambdaQueryWrapper<QuestionChoice>().in(QuestionChoice::getQuestionId, ids));
//        3.批量查询这些题目id下的所有答案
        List<QuestionAnswer> allAnswers = questionAnswerMapper.selectList(new LambdaQueryWrapper<QuestionAnswer>().in(QuestionAnswer::getQuestionId, ids));
//        4.按照题目id将选项分组
        Map<Long, List<QuestionChoice>> choicesMap = allchoices.stream().collect(Collectors.groupingBy(QuestionChoice::getQuestionId));//groupingBy(QuestionChoice::getQuestionId指按照题目id对选项进行分组
//        5.按题目id分组答案
        Map<Long, QuestionAnswer> answerMap = allAnswers.stream().collect(Collectors.toMap(QuestionAnswer::getQuestionId, a -> a));
//        - 知识点：2. toMap 和 groupingBy 的区别：
//        Collectors.toMap(...)：生成 Map<Key, Value>，其中 一个 Key 只能对应一个 Value。
//        如果出现重复的 Key，会直接抛出异常（默认行为）。正适合 “一个题目 ID 对应一个答案” 的场景，
//        直接用 QuestionAnswer::getQuestionId 作为 Key，QuestionAnswer 对象本身作为 Value，
//        清晰表达 “一对一” 的映射。
//        Collectors.groupingBy(...)：生成 Map<Key, List<Value>>，其中 一个 Key 可以对应多个 Value（自动封装成 List）。
//        适合 “一个题目 ID 对应多个选项” 的场景，所以选项用 groupingBy 分组后，得到的是 Map<Long,
//        List<QuestionChoice>>（每个题目 ID 对应多个选项的列表）。

//        6.把前面批量查询到的选项和答案，分别 “贴” 到对应的题目上
        for (Question question : questions) {  //循环遍历所有题目
            if ("CHOICE".equals(question.getType())){ //当题目类型是选择题时
                List<QuestionChoice> choices = choicesMap.getOrDefault(question.getId(), new ArrayList<>()); //从选项map中根据题目id挑出选项集合
//              选择题的选项（A、B、C、D）需要按顺序展示，这里根据选项的 sort 字段（排序号）进行升序排序（sort 为 null 时按 0 处理）。：
                choices.sort(Comparator.comparingInt(c->c.getSort() == null ? 0 : c.getSort()));
                question.setChoices(choices); //将某个选项们设置到该题目上
            }
//            7.给每个题目id赋予其对应答案
            question.setAnswer(answerMap.get(question.getId())); //
        }
    }


    /**
     *批量题目导入
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public String batchImportQuestions(List<QuestionImportVo> questionImportVoList) throws IOException {
        if (questionImportVoList.isEmpty()){
            throw new RuntimeException("Excel文件中没有有效的题目！");
        }
//        1.进行题目批量插入
        int successCount = 0; //定义成功插入的数量
        for (QuestionImportVo importVo : questionImportVoList) {
//            需要利用try避免中断：
            try {
//                转换DTO为实体：
                Question question = convertImportDtoToQuestion(importVo);
//                保存题目及其详情（选项以及答案）
                saveQuestionWithDetails(question);
                successCount++;
                log.info("成功导入题目:{}",question.getTitle());
            }catch (Exception e){
                log.error("导入题目失败:{},错误:{}",importVo.getTitle(),e.getMessage());
//                继续处理下一个题目，不中断整个导入过程：
            }
        }
        return String.format("Excel导入完成！成功导入 %d / %d 道题目",successCount,questionImportVoList.size());
    }
}